import math
import random

import algebra

import ode

import pygame
from pygame.locals import *

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import numpy

game = None


class Game:
    def __init__(self):
        global game
        game = self

        self.world = ode.World()
        self.world.setGravity((0.0, -9.8, 0.0))
        self.space = ode.Space()
        self.objects = []
        self.contacts = ode.JointGroup()

        self.timers = {}

        self.generators = []

        self.time = 0.0

        self.ball = Sphere(1, 0.5)
        self.ball.set_position((0.0, 3.0, 0.0))
        self.ball.set_texture_name("ball")
        self.add_object(self.ball)

        self.eye = numpy.array((0.0,6.0,10.0))
        self.rot_dir = numpy.array(())

        self.ball.geom.label = "ball"
        self.can_jump = False

        # load textures
        self.textures = {}
        self.textures["ball"] = self.loadTexture("data/textures/ball.bmp")
        self.textures["box"] = self.loadTexture("data/textures/wood.bmp")
        self.textures["p2"] = self.loadTexture("data/textures/stripe.bmp")
        self.textures["goal"] = self.loadTexture("data/textures/goal.bmp")
        self.textures["swirl"] = self.loadTexture("data/textures/swirl.bmp")
        self.textures["ice"] = self.loadTexture("data/textures/ice.bmp")

        
        # orange light
        glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.9, 0.45, 0.0, 1.0))
        glLightfv(GL_LIGHT0, GL_POSITION, (0.0, 60.0, -10.0, 1.0))

        # blue light
        glLightfv(GL_LIGHT1, GL_DIFFUSE, (0.0, 0.0, 0.5, 1.0))
        glLightfv(GL_LIGHT1, GL_POSITION, (0.0, 0.0, -2.0, 1.0))

        glEnable(GL_LIGHTING)
    
        glEnable(GL_LIGHT0)
        glEnable(GL_LIGHT1)


        glCullFace(GL_BACK)
        glEnable(GL_CULL_FACE)
        

        glEnable(GL_TEXTURE_2D)

        self.mipmap_enabled = True
        self.draw_silhouette = True
        self.camera_lag = True



    def loadTexture(self, file):
        image = pygame.image.load(file)
        data = pygame.image.tostring(image, "RGBX", 1)

        tex_id = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, tex_id)
        gluBuild2DMipmaps(GL_TEXTURE_2D, 3, image.get_width(),
                            image.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, data)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                            GL_LINEAR_MIPMAP_LINEAR)

        return tex_id


    def add_timer(self, name, interval, offset, callback):
        t = Timer(interval, offset, callback)
        self.timers[name] = t

    def set_start_pos(self, pos):
        self.ball.set_position(pos)

    def add_object(self, object):
        self.objects.append(object)

    def compute_camera_position(self):
        pos = numpy.array(self.ball.body.getPosition())
        
        d = numpy.array((0.0, 0.0))
        d[0] = self.eye[0]-pos[0]
        d[1] = self.eye[2]-pos[2]

        m = algebra.magnitude(d)

        r = 12.0/m

        new_eye = numpy.array((0.0, 0.0))
        new_eye[0] = pos[0] + r*d[0]
        new_eye[1] = pos[2] + r*d[1]

        self.eye[0] += 0.04*(new_eye[0]-self.eye[0])
        self.eye[2] += 0.04*(new_eye[1]-self.eye[2])


    def draw(self):

        pos = numpy.array(self.ball.body.getPosition())
        self.compute_camera_position()

        # obtain the camera rotation in the x,z plane
        glLoadIdentity()
        glPushMatrix()
        gluLookAt(self.eye[0], 0.0, self.eye[2], pos[0], 0.0, pos[2], 0.0, 1.0, 0.0)

        r = numpy.array(glGetDouble(GL_MODELVIEW_MATRIX))
        self.rot_dir = r[0:3,0:3]
        
        glPopMatrix()

        # setup the camera
        glLoadIdentity()
        glPushMatrix()
        gluLookAt(self.eye[0], pos[1]+self.eye[1], self.eye[2], pos[0], pos[1], pos[2], 0.0, 1.0, 0.0)

        # draw all objects in the game
        for o in self.objects:
            o.draw()
        
        glPopMatrix()



    def step(self, fps):
        self.space.collide(self, Game.collision_callback)
        self.world.step(1.0/fps)
        self.check_timers()
        
        self.contacts.empty()

        self.time += 1.0/fps
        
        # remove any objects that fall below a certain point
        for i in range(len(self.objects)-1, 0, -1):
            pos = self.objects[i].geom.getPosition()
            if pos[1] < -40.0:
                game.space.remove(self.objects[i].geom)
                del self.objects[i].body
                del self.objects[i]

    def check_timers(self):
        for name in self.timers:
            timer = self.timers[name]
            timer.tick(self.time)

    def do_input(self):
        event = pygame.event.poll()

        if event.type == QUIT:
            return True
        elif event.type == KEYDOWN:
            if event.key == K_ESCAPE or event.key == K_q:
                return True
            if event.key == K_f:
                pygame.display.toggle_fullscreen()
                pygame.mouse.set_visible(not pygame.mouse.set_visible(False))
       
            if event.key == K_l:
                self.draw_silhouette = not self.draw_silhouette

            if event.key == K_p:
                self.camera_lag = not self.camera_lag
            
            if event.key == K_t:
                if glIsEnabled(GL_TEXTURE_2D):
                    glDisable(GL_TEXTURE_2D)
                else:
                    glEnable(GL_TEXTURE_2D)

            if event.key == K_g:
                self.ball.body.setGravityMode(
                        not self.ball.body.getGravityMode())
 
            if event.key == K_m:
                if not self.mipmap_enabled:
                    self.mipmap_enabled = True
                    for t in self.textures:
                        glBindTexture(GL_TEXTURE_2D, self.textures[t])
                        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
                                            GL_LINEAR)
                        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                                            GL_LINEAR_MIPMAP_LINEAR)
                else:
                    self.mipmap_enabled = False
                    for t in self.textures:
                        glBindTexture(GL_TEXTURE_2D, self.textures[t])
                        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
                                            GL_NEAREST)
                        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                                            GL_NEAREST)

            if event.key == K_SPACE and self.can_jump:
                self.ball.body.addForce((0.0, 150.0, 0.0))

        self.can_jump = False

        keys = pygame.key.get_pressed()
      
        forward = numpy.array((0.0, 0.0, -4.0))
        backward = numpy.array((0.0, 0.0, 4.0))
        left = numpy.array((-4.0, 0.0, 0.0))
        right = numpy.array((4.0, 0.0, 0.0))
        
        # rotate movement vectors so that movement is always
        # relative to the orientation of the camera
        forward = numpy.dot(self.rot_dir, forward)
        backward = numpy.dot(self.rot_dir, backward)
        left = numpy.dot(self.rot_dir, left)
        right = numpy.dot(self.rot_dir, right)

        if keys[K_UP]:
            self.ball.body.addForce(forward)
        if keys[K_DOWN]:
            self.ball.body.addForce(backward)
        if keys[K_LEFT]:
            self.ball.body.addForce(left)
        if keys[K_RIGHT]:
            self.ball.body.addForce(right)


    def collision_callback(self, g1, g2):
        points = ode.collide(g1, g2)


        for p in points:
            p.setBounce(0.0)
            
            joint = ode.ContactJoint(self.world, self.contacts, p)
            joint.attach(g1.getBody(), g2.getBody())

            if g1.label == "ball" or g2.label == "ball":
                self.can_jump = True

                av = self.ball.body.getAngularVel()
                self.ball.body.addTorque(-0.12*numpy.array(av))


class Timer:

    def __init__(self, interval, offset, callback):
        self.interval = interval
        self.offset = offset
        self.callback = callback

    def tick(self, t):
        if t > (self.interval + self.offset):
            self.callback()
            self.offset = t

class BallGenerator:

    def __init__(self, name, freqs, pos, sizes, dens, texs):
        
        self.freqs = freqs
        self.pos = pos
        self.sizes = sizes
        self.dens = dens
        self.texs = texs
        self.name = name
        
        t = random.choice(self.freqs)
        game.add_timer(name, t, game.time, self.generate)

    def generate(self):
        
        t = random.choice(self.freqs)
        timer = Timer(t, game.time, self.generate)
        game.timers[self.name] = timer
        

        size = random.choice(self.sizes)
        s = Sphere(random.choice(self.dens), size)
        (xpos, ypos, zpos) = self.pos

        pos = (random.choice(xpos),
                random.choice(ypos),
                random.choice(zpos))

        s.set_position(pos)
        s.set_texture_name(random.choice(self.texs))

        game.add_object(s)
        


class Object:

    def __init__(self):
        self.geom = None
        self.body = None

        self.tex = ""

    def draw(self):
        (x,y,z) = self.geom.getPosition()

        glPushMatrix()
        glTranslatef(x, y, z)
        
        if isinstance(self, Sphere):
            f = self.body.getLinearVel()
            glScale(1.0+0.05*abs(f[0]), 1.0+0.05*abs(f[1]), 1.0+0.05*abs(f[2]))

        # obtain orientation of object and rotate it
        r = self.geom.getRotation()
        r = (r[0], r[3], r[6], 0.0,
             r[1], r[4], r[7], 0.0,
             r[2], r[5], r[8], 0.0,
             0.0,  0.0,  0.0,  1.0)
        glMultMatrixd(r)

        if self.tex != "":
            glBindTexture(GL_TEXTURE_2D, game.textures[self.tex])
        else:
            glBindTexture(GL_TEXTURE_2D, 0)
        
        # draw the object
        glCallList(self.display_list)

        # draw the silhouette

        tex_enabled = False
        if glIsEnabled(GL_TEXTURE_2D):
            tex_enabled = True
            glDisable(GL_TEXTURE_2D)

        glDisable(GL_LIGHTING) # disable lighting
        glPolygonMode(GL_BACK, GL_LINE) # set to draw wireframes
        glLineWidth(4.0) # set line width

        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)	# Use The Good Calculations
        glEnable(GL_LINE_SMOOTH)			# Enable Anti-Aliasing

        glCullFace(GL_FRONT) # don't draw front facing polygons

        glColor3f(0.0, 0.0, 0.0) # set silhouette colour

        # draw the silhouette if required
        if game.draw_silhouette:
            glCallList(self.display_list)
        
        glPopMatrix()
        # finished drawing

        glColor3f(1.0, 1.0, 1.0) # reset colour

        glCullFace(GL_BACK)
        glDisable(GL_LINE_SMOOTH)  # disable line smoothing
        glPolygonMode(GL_BACK, GL_FILL)
        glEnable(GL_LIGHTING)

        if tex_enabled:
            glEnable(GL_TEXTURE_2D)



    def set_texture_name(self, texture_name):
        self.tex = texture_name
 
    def set_position(self, pos):
        self.geom.setPosition(pos)
        


class Sphere(Object):
    def __init__(self, density, radius):
        Object.__init__(self)

        self.radius = radius
        self.body = ode.Body(game.world)
        M = ode.Mass()
        M.setSphere(density, radius)
        self.body.setMass(M)

        self.geom = ode.GeomSphere(game.space, radius)
        self.geom.setBody(self.body)

        self.geom.label = ""

        self.display_list = glGenLists(1)

        glNewList(self.display_list, GL_COMPILE)
        quadric = gluNewQuadric()
        gluQuadricTexture(quadric, GL_TRUE)
        gluSphere(quadric, self.radius, 16, 16)
        glEndList();


class StaticBox(Object):
    def __init__(self, size):
        Object.__init__(self)

        self.geom = ode.GeomBox(game.space, size)
        self.geom.label = ""
        
        self.body = None

        self.tex = "box"

        self.display_list = glGenLists(1)

        (sx, sy, sz) = self.geom.getLengths()
        sx = sx/2.0
        sy = sy/2.0
        sz = sz/2.0
        
        glNewList(self.display_list, GL_COMPILE)
        self.myCube(sx, sy, sz)
        glEndList();
        
        

    def set_angles(self, angles):
        r = algebra.rotation_matrix(angles)
        self.geom.setRotation(r.flatten())


    def myCube(self, sx, sy, sz):
        
        glBegin(GL_QUADS)
        
        # front face
        glNormal3f(0, 0, 1)
        glTexCoord2f(0, 0)
        glVertex3f(-sx, -sy, sz) # 0
        glTexCoord2f(1, 0)
        glVertex3f(sx, -sy, sz) # 1
        glTexCoord2f(1, 1)
        glVertex3f(sx, sy, sz) # 2
        glTexCoord2f(0, 1)
        glVertex3f(-sx, sy, sz) # 3
        
        # back face
        glNormal3f(0, 0, -1)
        glTexCoord2f(0, 0)
        glVertex3f(sx, -sy, -sz) # 5
        glTexCoord2f(1, 0)
        glVertex3f(-sx, -sy, -sz) # 4
        glTexCoord2f(1, 1)
        glVertex3f(-sx, sy, -sz) # 7
        glTexCoord2f(0, 1)
        glVertex3f(sx, sy, -sz) # 6

        # top face
        glNormal3f(0, 1, 0)
        glTexCoord2f(0, 0)
        glVertex3f(-sx, sy, sz) # 3
        glTexCoord2f(1, 0)
        glVertex3f(sx, sy, sz) # 2
        glTexCoord2f(1, 1)
        glVertex3f(sx, sy, -sz) # 6
        glTexCoord2f(0, 1)
        glVertex3f(-sx, sy, -sz) # 7

        # bottom face
        glNormal3f(0, -1, 0)
        glTexCoord2f(0, 0)
        glVertex3f(-sx, -sy, -sz) # 4
        glTexCoord2f(1, 0)
        glVertex3f(sx, -sy, -sz) # 5
        glTexCoord2f(1, 1)
        glVertex3f(sx, -sy, sz) # 1
        glTexCoord2f(0, 1)
        glVertex3f(-sx, -sy, sz) # 0

        # left face
        glNormal3f(-1, 0, 0)
        glTexCoord2f(0, 0)
        glVertex3f(-sx, -sy, -sz) # 4
        glTexCoord2f(1, 0)
        glVertex3f(-sx, -sy, sz) # 0
        glTexCoord2f(1, 1)
        glVertex3f(-sx, sy, sz) # 3
        glTexCoord2f(0, 1)
        glVertex3f(-sx, sy, -sz) # 7

        # right face
        glNormal3f(1, 0, 0)
        glTexCoord2f(0, 0)
        glVertex3f(sx, -sy, sz) # 1
        glTexCoord2f(1, 0)
        glVertex3f(sx, -sy, -sz) # 5
        glTexCoord2f(1, 1)
        glVertex3f(sx, sy, -sz) # 6
        glTexCoord2f(0, 1)
        glVertex3f(sx, sy, sz) # 2

        glEnd()


class Platform(StaticBox):
    def __init__(self, density, size):
        StaticBox.__init__(self, size)

        (sx, sy, sz) = size

        self.body = ode.Body(game.world)
        M = ode.Mass()
        M.setBox(density, sx, sy, sz)
        self.body.setMass(M)
        self.body.boxsize = size

        self.geom.setBody(self.body)
        
        self.body.setGravityMode(False)

    def set_angular_vel(self, vel):

        r = numpy.array(self.body.getRotation())
        r = numpy.resize(r, (3,3))

        v = numpy.array(vel)
        v = numpy.dot(r, v)
        self.body.setAngularVel(v)

    def set_linear_vel(self, vel):
        self.body.setLinearVel(vel)

    def get_linear_vel(self):
        return self.body.getLinearVel()


class MovingPlatform(Platform):
    def reverse(self):
        v = self.get_linear_vel()
        vp = (-v[0], -v[1], -v[2])
        self.set_linear_vel(vp)
